Skip to content

Conversation

@mmatous
Copy link
Contributor

@mmatous mmatous commented Apr 25, 2025

Purpose

Hi, this PR includes support for documenting PEP 695 type aliases. The main problem was that while looking for docstrings, Sphinx's parser didn't recognize ast.TypeAlias as visitable (for doc comment, missing visit_TypeAlias()) and TypeAliasType object wasn't recognized as something documentable when encountering docstring in visit_Expr() (for docstrings).

The rest of it is mostly just me guessing how stuff should be rendered into ReST and trying to ensure Python 3.11 compatibility.

I put the relevant autodoc parts into ClassDocumenter since that's where code for other type alias variants lives.

Couple of caveats:

  • Type aliases in signatures don't get cross-linked in HTML. I could use some pointers here. I think the ReST is mostly fine and the problem lies in HTML gen.? What should I do about that?

  • Generic type aliases do not get rendered as such. E.g. type A[T] = list[T] yields only type A = list[T] in resulting HTML. This seems expected, since there is no supported syntax for type params in py:type, unlike py:method

  • Does not include support for PEP 695 type parameters in generic classes (class ClassA[T: str]:) or functions/methods (def func[T](a: T, b: T) -> T:). I thought about it briefly and it seems more complicated than I'm willing to tackle rn.

  • How should I go about ruff accepting the test file? Add exemption to pyproject? SyntaxError bc of targeting Py3.11 can't be suppressed.

References

@mmatous mmatous force-pushed the pep695 branch 4 times, most recently from 89caa94 to ef7dbce Compare April 26, 2025 00:25
@AA-Turner
Copy link
Member

Thanks Martin! Please could you resolve conflicts?

cc also @picnixz if you want to have a look.

A

@mmatous
Copy link
Contributor Author

mmatous commented May 14, 2025

done

@picnixz picnixz self-requested a review May 15, 2025 17:05
Copy link
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised by how easy it was to add this support...

@picnixz
Copy link
Member

picnixz commented May 15, 2025

Type aliases in signatures don't get cross-linked in HTML. I could use some pointers here

That's probably because of how we parse the signature. This is a part I wrote so I could try to help you later this week (I'm sorry I got disconnected quite a lot with Sphinx now that I'm contributing directly to CPython).

type A[T] = list[T] yields only type A = list[T] in resulting HTML. This seems expected, since there is no supported syntax for type params in py:type, unlike py:method

Again that's me who wrote that part for method and the rest. We didn't have the type directive yet and I think I forgot to update this. There's an open issue somewhere where I said I'll take care of it but I forgot. My bad.

Does not include support for PEP 695 type parameters in generic classes (class ClassA[T: str]:)

I'm surprised: the parser actually supports this. But cross-referencing may not be entirely supported though.

@mmatous mmatous force-pushed the pep695 branch 3 times, most recently from 042c7ec to 74e759d Compare May 21, 2025 01:55
@mmatous
Copy link
Contributor Author

mmatous commented May 21, 2025

Updated with requested changes.

This is a part I wrote so I could try to help you later this week

The links would be nice. I'd like to incorporate them in this PR provided the necessary changes are small enough. If it's more work I'll still make them happen, but I wouldn't want this PR to be bogged down because of it. Just point me in the right direction for now. I'll figure it out.

):
self.add_line(' :canonical: %s' % canonical_fullname, sourcename)

if isinstance(self.object, TypeAliasType):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typing_extensions.TypeAliasType is a different object than typing.TypeAliasType (even on 3.12 where both exist).

See litestar-org/litestar#3982 (comment) for more details.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. Doesn't seem relevant in this context, though. Only one of them will be imported at any given time (0912a5e#diff-e43bdd6f8f37a12d2536e09e57c5e8999cb8de18b9c7ba49126f90576c4328acR57), so the parsed code will always be interpreted consistently as either one or the other.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way for the above if to be executed? also, not isinstance(self.object, NewType) is always true so we should remove it. I just don't want to add 2 canonical lines. So we should first check for TypeAliasType first maybe.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Python >= 3.12, it should match both typing.TypeAliasType and typing_extensions.TypeAliasType. Someone might explicitly create a typing_extensions.TypeAliasType object in order to create a type alias while still supporting Python < 3.12.

Copy link
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a lot of time, but here's another round of review.

CHANGES.rst Outdated
* #13704: autodoc: Detect :py:func:`typing_extensions.overload <typing.overload>`
and :py:func:`~typing.final` decorators.
Patch by Spencer Brown.
* #13508: Initial support for PEP 695 type aliases.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* #13508: Initial support for PEP 695 type aliases.
* #13508: Initial support for :pep:`695` type aliases.

):
self.add_line(' :canonical: %s' % canonical_fullname, sourcename)

if isinstance(self.object, TypeAliasType):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way for the above if to be executed? also, not isinstance(self.object, NewType) is always true so we should remove it. I just don't want to add 2 canonical lines. So we should first check for TypeAliasType first maybe.

def collect_doc_comment(
self,
# exists for >= 3.12, irrelevant for runtime
node: ast.Assign | ast.TypeAlias, # type: ignore[name-defined]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like this suppression so how about having an "AssignmentLike = ast.Assign" for < 3.12 and AssignmentLike = ast.Assign | ast.TypeAlias for >= 3.12 and use such annotation?

# check comments before assignment
if indent_re.match(current_line[: node.col_offset]):
comment_lines = []
for i in range(node.lineno - 1):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of going from 0 to node.lineno - 2 and compute node.lineno - 1 - i, why not using a reversed range?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code is just being moved, not changed.

Comment on lines 453 to 455
if (sys.version_info[:2] >= (3, 12)) and isinstance(
self.previous, ast.TypeAlias
):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (sys.version_info[:2] >= (3, 12)) and isinstance(
self.previous, ast.TypeAlias
):
elif (sys.version_info[:2] >= (3, 12)) and isinstance(
self.previous, ast.TypeAlias
):

@jbms
Copy link
Contributor

jbms commented Oct 5, 2025

Note: The issue from #10785 also applies to py:type directives --- the default Python type xref uses the class role which currently does not match py:type objects.

The easiest solution would be to add the class role to py:type objects --- that seems more appropriate than making type objects merely a fallback for class role lookups, since type objects are known to be types.

Alternatively, the type role could become the default type xref role, and it could match class and exception objects, and fall back to data and attr as class currently does --- the data and attr fallback for the class role could be removed since it hasn't been released yet.

Or we could introduce yet another role like anytype...

@jbms
Copy link
Contributor

jbms commented Oct 5, 2025

@mmatous It would be great to move this forward --- are you still interested in working on this?

I started my own monkey-patching implementation of this in the sphinx-immaterial theme here: jbms/sphinx-immaterial#467

A few differences are that I have also mapped PEP 613 X: TypeAlias = ... type aliases to the py:type directive also, and support type parameters (based on the existing support for type parameters in sphinx-immaterial).

The py:type directive and other Python domain directives already support the type parameter syntax but I'd suggest deferring proper support for type parameters to a later PR because some other significant changes may be needed to make xrefs to the type parameters work properly.

@AA-Turner
Copy link
Member

@jbms would you have time to open a rebased PR?

A

@jbms
Copy link
Contributor

jbms commented Oct 5, 2025

@jbms would you have time to open a rebased PR?

A

Yeah I'm actually working on rebasing and addressing other comments now.

jbms added 2 commits October 7, 2025 09:32
For Python 3.12 and 3.13, both exist and are not the same type.
@jbms
Copy link
Contributor

jbms commented Oct 7, 2025

I believe I've now addressed all of the review comments and outstanding issues. Type parameters are still not handled, but that is largely orthogonal to PEP 695 type alias support.

@jbms
Copy link
Contributor

jbms commented Oct 7, 2025

There is still one issue remaining with how type alias objects are stringified in stringify_annotation that I need to look into.

@jbms
Copy link
Contributor

jbms commented Oct 8, 2025

There is still one issue remaining with how type alias objects are stringified in stringify_annotation that I need to look into.

I think it is okay as is actually.

The issue is that you can define a type alias as a class member, e.g.:

class Foo:
    type X = int

However, the resultant TypeAliasType object has a __module__ and __name__ but does not have a __qualname__ so stringify_annotation just outputs modulename.X rather than modulename.Foo.X.

In principle, inside of stringify_annotation we could attempt to figure out a qualified path to X, by first checking if it is defined at the top level of the module, and if not, attempt to build a (cached) dict mapping TypeAliasType objects within the module to their corresponding qualified names, by attempting to iterate over all nested classes defined within the module and then iterating over their class variables.

However, the same issue also applies to NewType objects (which are already supported by Sphinx) and this would add significant additional complexity to stringify_annotation just to handle this edge case.

It might be nice to add a note about these caveats to the autodoc documentation, though.

@jbms
Copy link
Contributor

jbms commented Oct 8, 2025

@AA-Turner I think this is ready now.

@AA-Turner
Copy link
Member

Thanks Jeremy, I've pushed some final adjustments.

One last question from me -- should we include type statements under .. autoclass::, or create a new e.g. .. autotype:: / .. autotypealias:: directive? This PR does the former, I presume partly for simplicity, but I wonder if it'd be better to keep them separate?

cc @picnixz @jbms

A

@jbms
Copy link
Contributor

jbms commented Oct 9, 2025

I had intended that explicitly created typing_extensions.TypeAliasType objects would still be supported even on Python < 3.12.

As far as using autoclass vs a separate directive:

The main reason to keep it as autoclass:

  • Consistent with the handling of NewType, which is conceptually pretty similar.
  • Consistent with the handling of TypeVar (although these really should be handled in a different way, as special typeParameter objects that get created automatically by the Python domain when signatures contain type parameters, as in sphinx-immaterial).
  • Relatively simple implementation-wise.
  • Autoclass already has the "doc_as_attrlogic for aliases, which currently results in apy:dataorpy:attributebut arguably should instead be documented aspy:type`.

Reasons to keep it as a separate directive:

  • All of the directive options allowed for autoclass are not applicable to type aliases, nor is the signature syntax applicable.

If we add an autotype directive, arguably it should also handle NewType. Perhaps we should modify the py:type directive to better support NewType, e.g. display NewType("X", int) as newtype X = int. However, for backwards compatibility autoclass would also still need to handle NewType.

Overall it seems like making a separate autotype directive is the way to go.

@AA-Turner
Copy link
Member

I had intended that explicitly created typing_extensions.TypeAliasType objects would still be supported even on Python < 3.12.

@jbms they still are, no? I thought that there was a test for this.

@AA-Turner
Copy link
Member

Overall it seems like making a separate autotype directive is the way to go.

I've now split type aliases into a new directive, which I prefer. You make a good point on directive options.

A

Copy link
Member

@AA-Turner AA-Turner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll get this in now so that it doesn't linger further. Thanks @mmatous for opening the initial draft and to @jbms for helping get it accross the line!

Jeremy -- if my changes accidentally broke typing_extensions.TypeAliasType on 3.11 & earlier, sorry -- please could you open a new issue/PR if this is the case?

A

@AA-Turner AA-Turner merged commit ad3f3cc into sphinx-doc:master Oct 10, 2025
30 checks passed
@jbms
Copy link
Contributor

jbms commented Oct 10, 2025

@AA-Turner thanks!

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 8, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants